/*
Bugs:
non-closed static polygons

To Do:
clarify ie6 and quirks
*/

(function()
{

// Object that handles the common functions about all the maps
function GoogleMapsHandler(editor)
{
	this.editor = editor;
	// Clean up
	editor.on( 'destroy', function() { this.maps = {}; this.editor = null;});

	// Object to store a reference to each map
	this.maps = {};

	// We will use this to track the number of maps that are generated
	// This way we know if we must add the Google Script or not.
	// We store their names so they are called properly from BuildEndingScript
	this.CreatedMapsNames = [];

	return this;
}

GoogleMapsHandler.prototype  = {
	majorVersion : 3,
	minorVersion : 3,

	isStaticImage: function( node ) {
		return node.getAttribute( 'mapnumber' ) || (/^(http\:)?\/\/maps\.google(apis)?\.com\/(maps\/api\/)?staticmap/).test( node.src );
	},

	getMap: function( id ){
		return this.maps[id];
	},

	// Verify that the node is a script generated by this plugin.
	detectMapScript: function( script )
	{
		if ( !(/CK googlemaps v(\d)\.(\d+)/.test(script)) )
			return false;

		var version = parseInt(RegExp.$1, 10),
			revision = parseInt(RegExp.$2, 10);

		if ( version>this.majorVersion || (version==this.majorVersion && revision>this.minorVersion) )
			return false;

		return true;
	},

	// Detects both the google script as well as our ending block
	// both must be removed and then added later only if necessary
	detectGoogleScript: function( script )
	{
		// Our final script
		if (/CK googlemapsEnd v(\d)\.(\d+)/.test(script) )
		{
			var version = parseInt(RegExp.$1, 10),
				revision = parseInt(RegExp.$2, 10);

			if ( version>this.majorVersion || (version==this.majorVersion && revision>this.minorVersion) )
				return false;

			if (version==2)
			{
				if ( !(/script.src\s*=\s*"http:\/\/www\.google\.com\/.*key=(.*?)("|&)/).test(script) )
					return false;
			}
			return true;
		}
		// Check if it is the Google Maps script (but we don't need the key)
		if ( !(/^<script src="http:\/\/maps\.google\.com\/.*key=(.*?)("|&)/).test(script) )
			return false;

		return true;
	},

	// This can be called from the dialog
	createNew: function()
	{
		var map = new CKEGoogleMap( this.editor );
		this.maps[ map.number ] = map;
		return map;
	},

	BuildEndingScript: function()
	{
		var aScript = [],
			path = this.editor.config.googleMaps_endScriptPath;

		// Empty string: don't insert it at all. It must be inserted in the page template
		if (path==="")
			return "";

		// Not defined: use default path
		if (typeof path == "undefined")
			path = this.editor.plugins.googlemaps.path + "scripts/cke_gmaps_end.js";

		aScript.push('\r\n<script type="text/javascript" src="' + path + '">');
		aScript.push( '/* CK googlemapsEnd v' + this.majorVersion + '.' + this.minorVersion + ' */' );
		aScript.push('</script>');

		return aScript.join('');
	},

	// Function that will test if the divs and images are correctly wrapped
	// returns true if it was OK, false if it has modified the DOM
	validateDOM: function()
	{
		// if no maps have been used, then exit
		var tmpMap, aMaps = [], allImages, i, j, imgs, parent, mapId, map, div, img, element,
			modified = false,
			maps = this.maps;

		for (tmpMap in maps)
			aMaps.push( tmpMap );

		if (aMaps.length===0)
			return true;

		var theDoc = this.editor.document.$;
		// the user might have moved an img outside its container div.
		for(i=0; i<aMaps.length; i++)
		{
			mapId = aMaps[i];
			map = maps[mapId];
			img = theDoc.getElementById( mapId );
			if ( img )
			{
				if (img.parentNode.nodeName != 'DIV' || img.parentNode.id != 'd' + map.number)
				{
					modified = true;
					map.createDivs( img );
				}
				map.updateDimensions( img );
			}
			else
			{
				div = theDoc.getElementById( 'd' + mapId );
				if (div)
				{
					element = new CKEDITOR.dom.element( div );
					element.remove( true );
					modified = true;
				}
				delete maps[ mapId ];
			}
		}

		// Try to check if the container divs have been duplicated (for example by drag&drop)
		if ( document.querySelectorAll )
		{
			for (i=0; i<aMaps.length; i++)
			{
				mapId = aMaps[i];
				var divs = theDoc.querySelectorAll("div#d" + mapId);
				if (divs.length<=1)
					continue;

				//Ups, there are two or more divs, only one must remain
				for (j=divs.length-1; j>=0; j--)
				{
					div = divs[j];
					imgs = div.getElementsByTagName( 'IMG' );
					if ( !imgs || !imgs[0] || imgs[0].id != mapId)
					{
						// If it doesn't have an image inside, remove that div
						// but leave whatever it's inside, just in case...
						element = new CKEDITOR.dom.element( div );
						element.remove( true );
						modified = true;
					}
				}
			}
		}

		return !modified;
	}
};




// Our object that will handle parsing of the script and creating the new one.
function CKEGoogleMap( editor )
{
	this.editor = editor;

	var now = new Date();
	this.number = 'gmap' + now.getFullYear() + now.getMonth() + now.getDate() + now.getHours() + now.getMinutes() + now.getSeconds();

	this.width = editor.config.googleMaps_Width || 500;
	this.height = editor.config.googleMaps_Height || 370;

	this.centerLat = editor.config.googleMaps_CenterLat || 37.4419;
	this.centerLon =  editor.config.googleMaps_CenterLon || -122.1419;
	this.zoom = editor.config.googleMaps_Zoom || 11;

	this.markerPoints = [];
	this.textsData = [];
	this.linesData = [];
	this.areasData = [];

	this.mapType = 0; // map, satellite, hybrid...
	this.generatedType = 3; // 1: only static, 2: static + click to load, 3: load with page

	this.kmlOverlay = '';
	this.zoomControl = 'Default';
	this.mapTypeControl = 'Default';
	this.scaleControl = false;

	this.overviewMapControl = false;
	this.overviewMapControlOpened = true;
	this.googleBar = false;

	this.weather = false;
	this.traffic = false;
	this.transit = false;

	this.WrapperClass = editor.config.googleMaps_WrapperClass || '';
}


CKEGoogleMap.prototype.createHtmlElement = function()
{
	// http://dev.ckeditor.com/ticket/6178
/*
	if (CKEDITOR.env.webkit)
	{
		this.editor.focus();
	}
*/
	var oFakeNode = this.editor.document.createElement( 'IMG' );
	oFakeNode.setAttribute( 'mapnumber', this.number );
	oFakeNode.setAttribute( 'id', this.number );

	var oDiv = this.editor.document.createElement( 'DIV' );
	oDiv.setAttribute( 'id', 'd' + this.number);

	if (this.WrapperClass!=='')
	{
		var oWrapper = this.editor.document.createElement( 'DIV' );
		oWrapper.setAttribute( 'class', this.WrapperClass );
		this.editor.insertElement( oWrapper );
		oWrapper.append( oDiv );
	}
	else
		this.editor.insertElement( oDiv );

	oDiv.append( oFakeNode );
	return oFakeNode.$;
};

CKEGoogleMap.prototype.createDivs = function( oFakeNode )
{
	var oWrapper,
		oDiv = this.editor.document.$.createElement( 'DIV' );
	oDiv.setAttribute( 'id', 'd' + this.number);
	oFakeNode.parentNode.insertBefore( oDiv, oFakeNode );

	if (this.WrapperClass!=='')
	{
		oWrapper = this.editor.document.$.createElement( 'DIV' );
		oWrapper.className = this.WrapperClass;
		oDiv.parentNode.insertBefore( oWrapper, oDiv );
		oWrapper.appendChild( oDiv );
	}

	oDiv.appendChild( oFakeNode );
};

CKEGoogleMap.prototype.updateHTMLElement = function( oFakeNode )
{
	oFakeNode.setAttribute('width', this.width);
	oFakeNode.setAttribute('height', this.height);

	// Static maps preview :-)
	var staticMap = this.generateStaticMap();
	oFakeNode.setAttribute( 'src', staticMap);
	oFakeNode.setAttribute( 'data-cke-saved-src', staticMap);
};

CKEGoogleMap.prototype.getMapTypeIndex = function( type )
{
	return CKEDITOR.tools.indexOf(['roadmap', 'satellite', 'hybrid', 'terrain'], type);
};

CKEGoogleMap.prototype.parseStaticMap = function( oImage )
{
	var reStatic = /center=(-?\d+\.\d*),(-?\d+\.\d*)&zoom=(\d+)&size=(\d+)x(\d+)&maptype=(.*?)(?:&markers=(.*?))?(&path=(?:.*?))?/,
			markers, paths, result,
			reMarks = /(-?\d+\.\d*),(-?\d+\.\d*),(\w+)\|?/g,
			reLines = /rgba:0x(\w{6})(\w\w),weight:(\d)(.*?)(&|$)/g;

	if (reStatic.test( oImage.src ) )
	{
		this.generatedType = 1;

		this.centerLat = RegExp.$1;
		this.centerLon = RegExp.$2;
		this.zoom = RegExp.$3;
		this.width = oImage.width;
		this.height = oImage.height;
		this.mapType = this.getMapTypeIndex(RegExp.$6);
		// markers
		markers = RegExp.$7;
		// paths
		paths = RegExp.$8;

		while( (result = reMarks.exec( markers )) )
		{
			this.markerPoints.push( {lat:result[1], lon:result[2], text:'', color:result[3], title:'', maxWidth:200} );
		}
		while( (result = reLines.exec( paths )) )
		{
			this.linesData.push( {color:'#' + result[1], opacity:parseInt(result[2], 16)/255, weight:parseInt(result[3], 10), PointsData:result[4], points:null, text:'', maxWidth:200} );
		}

	}
};

CKEGoogleMap.prototype.parseStaticMap2 = function( oImage )
{
	var reStatic = /center=(-?\d+\.\d*),(-?\d+\.\d*)&zoom=(\d+)&size=(\d+)x(\d+)&maptype=(\w*?)(&markers=.*?)?(&path=.*?)?&sensor=false/,
			markers, paths, result, line,
			reMarks = /markers=color:(\w*)\|(-?\d+\.\d*),(-?\d+\.\d*)(&|$)/g,
			reLines = /path=color:0x(\w{6})(\w\w)\|weight:(\d)\|(fillcolor:0x(\w{6})(\w\w)\|)?enc:(.*?)&pathData=zf:(\d*)\|nl:(\d*)\|lvl:(.*?)(&|$)/g;

	if (reStatic.test( oImage.src ) )
	{
		this.generatedType = 1;

		this.centerLat = RegExp.$1;
		this.centerLon = RegExp.$2;
		this.zoom = RegExp.$3;
		this.width = oImage.width;
		this.height = oImage.height;
		this.mapType = this.getMapTypeIndex(RegExp.$6);
		// markers
		markers = RegExp.$7;
		// paths
		paths = RegExp.$8;

		while( (result = reMarks.exec( markers )) )
		{
			this.markerPoints.push( {lat:result[2], lon:result[3], text:'', color:result[1], title:'', maxWidth:200} );
		}

		while( (result = reLines.exec( paths )) )
		{
			line = {color:'#' + result[1], opacity:parseInt(result[2], 16)/255, weight:parseInt(result[3], 10), points:unescape(result[7])};

			if (result[4]) //polygons
			{
				this.areasData.push( {polylines: [line], fill:line.color, color:'#' + result[5], opacity:parseInt(result[6], 16)/255, text:'', maxWidth:200 } );
			}
			// lines
			else
			{
				line.text='';
				line.maxWidth = 200;
				this.linesData.push( line );
			}
		}
	} else {
			// Static Maps V1
			this.parseStaticMap( oImage );
	}
};

CKEGoogleMap.prototype.generateStaticMap = function()
{
	var w = Math.min(this.width, 640),
		h = Math.min(this.height, 640),
		staticMapTypes = ['roadmap', 'satellite', 'hybrid', 'terrain'];

	return '//maps.googleapis.com/maps/api/staticmap?center=' + this.centerLat + ',' + this.centerLon +
		'&zoom=' + this.zoom + '&size=' + w + 'x' + h +
		'&maptype=' + staticMapTypes[ this.mapType ] +
		this.generateStaticMarkers() +
		this.generateStaticPaths() +
		'&sensor=false';
};

CKEGoogleMap.prototype.generateStaticMarkers = function()
{
	if (this.markerPoints.length===0)
		return '';

	var i=0, point,
		aPoints = [],
		googleMaps_Icons = this.editor.config.googleMaps_Icons;

	for (; i<this.markerPoints.length; i++)
	{
		point = this.markerPoints[i];
		aPoints.push('&markers=');
		// Custom icons
		if ( googleMaps_Icons && googleMaps_Icons[ point.color ] )
			aPoints.push('icon:' + googleMaps_Icons[ point.color ].marker.image);
		else
			aPoints.push('color:' + point.color);

		aPoints.push('|' + point.lat + ',' + point.lon );
	}
	return aPoints.join('');
};

CKEGoogleMap.prototype.generateStaticPaths = function()
{
	function hex(n)
	{
		var h = Math.round(255 * n).toString(16);
		if (h.length==1)
			h = "0" + h;
		return h;
	}

	var strings = '', i=0, line, area;
	for (; i<this.linesData.length; i++)
	{
		// transform the color to rgba....
		line = this.linesData[i];
		strings += '&path=' + 'color:0x' + line.color.replace('#', '') + hex(line.opacity) + '|weight:' + line.weight + '|enc:' + line.points;
	}

	// Polygons
	for (i=0; i<this.areasData.length; i++)
	{
		area = this.areasData[i];
		line = area.polylines[0];
		strings += '&path=' + 'color:0x' + line.color.replace('#', '') + hex(line.opacity) + '|weight:' + line.weight + '|fillcolor:0x' + area.color.replace('#', '') + hex(area.opacity) + '|enc:' + line.points;
	}

	return strings;
};

// Read the dimensions back from the fake node (the user might have manually resized it)
CKEGoogleMap.prototype.updateDimensions = function( oFakeNode )
{
	var iWidth, iHeight,
		regexSize = /^\s*(\d+)px\s*$/i,
		aMatchW, aMatchH;

	if ( oFakeNode.style.width )
	{
		aMatchW  = oFakeNode.style.width.match( regexSize );
		if ( aMatchW )
		{
			iWidth = aMatchW[1];
			oFakeNode.style.width = '';
			oFakeNode.width = iWidth;
		}
	}

	if ( oFakeNode.style.height )
	{
		aMatchH  = oFakeNode.style.height.match( regexSize );
		if ( aMatchH )
		{
			iHeight = aMatchH[1];
			oFakeNode.style.height = '';
			oFakeNode.height = iHeight;
		}
	}

	this.width	= iWidth ? iWidth : oFakeNode.width;
	this.height	= iHeight ? iHeight : oFakeNode.height;
};

CKEGoogleMap.prototype.decodeText = function(string)
{
	return string.replace(/<\\\//g, "</").replace(/\\n/g, "\n").replace(/\\'/g, "'").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
};
CKEGoogleMap.prototype.encodeText = function(string)
{
	return string.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/<\//g, "<\\/");
};

CKEGoogleMap.prototype.parse = function( script )
{
	// Get version info:
	if ( !(/CK googlemaps v(\d+)\.(\d+)/.test(script)) )
		return false;

	var regexpKml = /google\.maps\.GeoXml\("([^"]*)"\)/ ,
		majorVersion = parseInt(RegExp.$1, 10) ,
		minorVersion = parseInt(RegExp.$2, 10) ,
		regexpId = /dMap\s=\sdocument\.getElementById\("(.*)"\)/ ,
		regexpWidth = /dMap\.style\.width\s=\s"(.*?)"/ ,
		regexpHeight = /dMap\.style\.height\s=\s"(.*?)"/ ,
		regexpType = /\/(?:\/|\*)generatedType = (\d)/ ,
		maxWidth ,
		regexpPosition = /map\.setCenter\(new google\.maps\.LatLng\((-?\d{1,3}\.\d{1,6}),(-?\d{1,3}\.\d{1,6})\), (\d{1,2})\);/ ,
		markerText, markerLat=0, markerLon=0 ,
		regexpText = /var text\s*=\s*("|')(.*)\1;\s*\n/ ,
		regexpDimensions = /<div id="(.*)" style="width\:\s*(\d+)px; height\:\s*(\d+)px;/ ,
		regexpMarker = /var point\s*=\s*new GLatLng\((-?\d{1,3}\.\d{1,6}),(-?\d{1,3}\.\d{1,6})\)/, // v< 1.5
		regexpMarkers = /\{lat\:(-?\d{1,3}\.\d{1,6}),\s*lon\:(-?\d{1,3}\.\d{1,6}),\s*text\:("|')(.*?)\3(?:,\s*color:"(.*?)"(?:,\s*title:"(.*?)"(?:,\s*maxWidth:(\d+))?)?)?}(?:,|])/g ,
		linePoints = '' ,
		lineLevels = '' ,
		regexpLinePoints = /var encodedPoints\s*=\s*("|')(.*)\1;\s*\n/ ,
		regexpLineLevels = /var encodedLevels\s*=\s*("|')(.*)\1;\s*\n/ ,
		color = 'red' ,
		title = '' ,
		regexpLines = /\{color\:'(.*)',\s*weight:(\d+),\s*opacity:(-?\d\.\d{1,2}),\s*points\:'(.*?)',\s*levels\:'(.*?)',\s*zoomFactor:(\d+),\s*numLevels:(\d+),\s*text\:'(.*?)'(?:,\s*maxWidth:(\d+))?}(?:,|])/g ,
		regexpAreas = /\{polylines:\s*\[\{color\:'(.*)',\s*opacity:(-?\d\.\d{1,2}),\s*weight:(\d+),\s*points\:'(.*)',\s*levels\:'(.*)',\s*zoomFactor:(\d+),\s*numLevels:(\d+)\}\],\s*fill:(.*),\s*color:'(.*)',\s*opacity:(-?\d\.\d{1,2}),\s*outline:(.*?),\s*text\:'(.*?)'(?:,\s*maxWidth:(\d+))?\}(?:,|])/g ,
		lines = [] ,
		regexpTexts = /\{lat\:(-?\d{1,3}\.\d{1,6}),\s*lon\:(-?\d{1,3}\.\d{1,6}),\s*title\:"(.*?)",\s*className:"(.*?)"}(?:,|])/g;

	var handler = this.editor.plugins.googleMapsHandler;

	if (majorVersion>3)
		return false;

	if (majorVersion>2)
		regexpKml =  /AddKml\("([^"]*)"\)/;

	if (majorVersion>=3 && minorVersion>=3)
		regexpMarkers = /\{lat\:(-?\d{1,3}\.\d{1,6}),\s*lon\:(-?\d{1,3}\.\d{1,6}),\s*text\:("|')(.*?)\3,\s*color:"(.*?)",\s*title:"(.*?)",\s*maxWidth:(\d+),\s*open:(\d)}(?:,|])/g ;

	// dimensions:
//	document.writeln('<div id="gmap1" style="width: 544px; height: 350px;">.</div>');
	if (majorVersion<2)
	{
		if (regexpDimensions.test( script ) )
		{
			delete handler.maps[this.number];
			this.number = RegExp.$1;
			handler.maps[this.number] = this;

			this.width = RegExp.$2;
			this.height = RegExp.$3;
		}
	}
	else
	{
//			var dMap = document.getElementById("gmap200893233020");
//			dMap.style.width = "300px";
//			dMap.style.height = "200px";
			if (regexpId.test( script ) )
			{
				delete handler.maps[this.number];
				this.number = RegExp.$1;
				handler.maps[this.number] = this;
			}

			if (regexpWidth.test( script ) )
				this.width = RegExp.$1.replace(/px$/, '');

			if (regexpHeight.test( script ) )
				this.height = RegExp.$1.replace(/px$/, '');

			if (regexpType.test( script ) )
				this.generatedType = RegExp.$1;
	}

//	map.setCenter(new GLatLng(42.4298,-8.07756), 8);
	if (majorVersion==1)
		regexpPosition = /map\.setCenter\(new GLatLng\((-?\d{1,3}\.\d{1,6}),(-?\d{1,3}\.\d{1,6})\), (\d{1,2})\);/;

	if (regexpPosition.test( script ) )
	{
		this.centerLat = RegExp.$1;
		this.centerLon = RegExp.$2;
		this.zoom = RegExp.$3;
	}

// v <= 1.5
	if (majorVersion==1 && minorVersion<=5 )
	{
	//	var text = 'En O Carballino ha estado la d\'elegacion diplomatica japonesa';
		if (regexpText.test( script ) )
		{
			markerText = RegExp.$2;
		}

	//	var point = new GLatLng(42.4298,-8.07756);
		if (regexpMarker.test( script ) )
		{
			markerLat = RegExp.$1;
			markerLon = RegExp.$2;
		}
		if (markerLat!==0 && markerLon!==0)
			this.markerPoints.push( {lat:markerLat, lon:markerLon, text:this.decodeText(markerText), color:'red', title:''} );
	}
	else
	{
	// v > 1.5. multiple points.

	// AddMarkers( [{lat:37.45088, lon:-122.21123, text:'Write your text'}] );
		while( (result = regexpMarkers.exec(script)) )
		{
			maxWidth = 200;
			var opened=false;
			if (result[5])
				color = result[5];
			if (result[6])
				title = result[6];
			if (result[7])
				maxWidth = result[7];
			if (result[8])
				opened = (result[8]=="1");
			this.markerPoints.push( {lat:result[1], lon:result[2], text:this.decodeText(result[4]), color:color, title:this.decodeText(title), maxWidth:maxWidth, open:opened} );
		}

		// Texts v>=2.1
		while( (result = regexpTexts.exec(script)) )
		{
			this.textsData.push( {lat:result[1], lon:result[2], title:this.decodeText(result[3]), className:this.decodeText(result[4])} );
		}
	}

	if (majorVersion==1)
	{
		// Notify back to parser that it must insert an img
		this.requiresImage = true;

		if (regexpLinePoints.test( script ) )
			linePoints = RegExp.$2;

	//	var encodedLevels = "B????????????????????????????????????B";
		if (regexpLineLevels.test( script ) )
			lineLevels = RegExp.$2;

		if (linePoints !== '' && lineLevels !== '')
		{
			this.linesData.push({color:'#3333cc', weight:5, opacity:0.45, points:linePoints, text:'', maxWidth:200 });
		}
	}
	else
	{
		// V2: multiple lines and areas.
//			aLines.push('{color:\'' + line.color + '\', weight:' + line.weight + ', points:\'' + line.points + '\', levels:\'' + line.levels + '\'}');
//		}
//		aScript.push('	AddLines( map, [' + aLines.join(',\r\n') + '] );');

		if (majorVersion<3 || (majorVersion==3 && minorVersion===0))
		{
			while( (result = regexpLines.exec(script)) )
			{
				maxWidth = 200;
				if (result[9])
					maxWidth = result[9];
				this.linesData.push( {color:result[1], weight:result[2], opacity:result[3], points:this.decodeText(result[4]), text:this.decodeText(result[8]), maxWidth:maxWidth} );
			}

			// only one line in each area
			// we reverse the opacity and weight to be able to avoid parsing them as normal polylines
	//	AddAreas( map, [{polylines: [{color:'#008000', opacity:0.7, weight:2, points:'iatcFnzrhVpfA{nOtuExkJnd@ffOwbIecJ', levels:'BBBBB'}], fill: true, color:'#008000', opacity:0.2, outline:true}] );
			while( (result = regexpAreas.exec(script)) )
			{
				lines = [{color:result[1], weight:parseInt(result[3],10), opacity:parseFloat(result[2]), points:this.decodeText(result[4])} ];

				maxWidth = 200;
				if (result[13])
					maxWidth = parseInt(result[13], 10);
				this.areasData.push( {polylines: lines, fill:true, color:result[9], opacity:parseFloat(result[10]), text:this.decodeText(result[12]), maxWidth:maxWidth } );
			}
		}
		else
		{
			// withouth levels, zoomfactor and numLevels params, but always with maxWidth
			regexpLines = /\{color\:("|')(.*)\1,\s*weight:(\d+),\s*opacity:(-?\d\.\d{1,2}),\s*points\:("|')(.*?)\5,\s*text\:("|')(.*?)\7,\s*maxWidth:(\d+)}(?:,|])/g ;
			regexpAreas = /\{polylines:\s*\[\{color\:("|')(.*)\1,\s*opacity:(-?\d\.\d{1,2}),\s*weight:(\d+),\s*points\:("|')(.*)\5}\],\s*fill:(.*),\s*color:("|')(.*)\8,\s*opacity:(-?\d\.\d{1,2}),\s*text\:("|')(.*?)\11,\s*maxWidth:(\d+)\}(?:,|])/g ;

			while( (result = regexpLines.exec(script)) )
			{
				this.linesData.push( {color:result[2], weight:parseInt(result[3],10), opacity:parseFloat(result[4]), points:this.decodeText(result[6]), text:this.decodeText(result[8]), maxWidth:result[9]} );
			}

			while( (result = regexpAreas.exec(script)) )
			{
				lines = [{color:result[2], opacity:parseFloat(result[3]), weight:parseInt(result[4],10), points:this.decodeText(result[6])} ];
				this.areasData.push( {polylines:lines, fill:true, color:result[9], opacity:parseFloat(result[10]), text:this.decodeText(result[12]), maxWidth:parseInt(result[13],10) } );
			}

		}

	}

// 1.8 mapType
//	map.setMapType( allMapTypes[ 1 ] );
	if (/setMapType\([^\[]*\[\s*(\d+)\s*\]\s*\)/.test( script ) )
	{
		this.mapType = RegExp.$1;
	}

// 1.9 wrapper div with custom class
	if ( majorVersion==1 && minorVersion >= 9 )
	{
		if (/<div class=("|')(.*)\1.*\/\/wrapper/.test( script ) )
			this.WrapperClass = RegExp.$2;
		else
			this.WrapperClass = '';
	}

	if (regexpKml.test( script ) )
	{
		this.kmlOverlay = RegExp.$1;
	}

	// controls:
	if ( majorVersion==2 )
	{
		if (/google\.maps\.(.*)\(\)\);\/\*zoom\*\//.test( script ) )
			this.zoomControl = RegExp.$1;
		else
			this.zoomControl = '';

		if (/google\.maps\.(.*)\(\)\);\/\*type\*\//.test( script ) )
			this.mapTypeControl = RegExp.$1;
		else
			this.mapTypeControl = '';

		this.overviewMapControl = (script.indexOf('OverviewMapControl')>=0);
		this.scaleControl = (script.indexOf('ScaleControl')>=0);
		this.googleBar = (script.indexOf('enableGoogleBar')>=0);

		// Remap them to v3:
		switch (this.zoomControl)
		{
			case 'SmallZoomControl':
				this.zoomControl = 'Small';
				break;
			case 'SmallMapControl':
				this.zoomControl = 'Small';
				break;
			case 'LargeMapControl':
				this.zoomControl = 'Full';
				break;
			default:
				this.zoomControl = 'None';
				break;
		}
		switch (this.mapTypeControl)
		{
			case 'MapTypeControl':
				this.mapTypeControl = 'Full';
				break;
			case 'HierarchicalMapTypeControl':
				this.mapTypeControl = 'Menu';
				break;
			default:
				this.mapTypeControl = 'None';
				break;
		}
	}
	// controls:
	if ( majorVersion==3 )
	{
/*
	var mapOptions = {
		zoom: 7,
		center: [42.7228,-1.72485],
		mapType: 0,
		navigationControl: "Full",
		mapsControl: "Menu",
		overviewMapControl: false,
		scaleControl: false,
		googleBar: false
	};
*/
		if (/center:\s*\[(-?\d{1,3}\.\d{1,6}),(-?\d{1,3}\.\d{1,6})\],/.test( script ) )
		{
			this.centerLat = RegExp.$1;
			this.centerLon = RegExp.$2;
		}

		if (/zoom: (\d{1,2}),/.test( script ) )
			this.zoom = parseInt(RegExp.$1, 10);

		if (/mapType: (\d{1,2}),/.test( script ) )
			this.mapType = parseInt(RegExp.$1, 10);

		if (/navigationControl: "(.*?)",/.test( script ) )
			this.zoomControl = RegExp.$1;

		if (/mapsControl: "(.*?)",/.test( script ) )
			this.mapTypeControl = RegExp.$1;

		if (/overviewMapControl: (.*?),/.test( script ) )
			this.overviewMapControl = (RegExp.$1 == 'true');

		if (/overviewMapControlOptions: \{opened:(.*?)\},/.test( script ) )
			this.overviewMapControlOpened = (RegExp.$1 == 'true');

		if (/scaleControl: (.*?),/.test( script ) )
			this.scaleControl = (RegExp.$1 == 'true');

		if (/traffic: (.*?),/.test( script ) )
			this.traffic = (RegExp.$1 == 'true');

		if (/weather: (.*?),/.test( script ) )
			this.weather = (RegExp.$1 == 'true');

		if (/transit: (.*?),/.test( script ) )
			this.transit = (RegExp.$1 == 'true');

		// last one without trailing comma
		if (/googleBar: (.*?)\n/.test( script ) )
			this.googleBar = (RegExp.$1 == 'true');

		// extra icons
		if ( minorVersion >= 3 )
		{
			if (!this.editor.config.googleMaps_Icons)
				this.editor.config.googleMaps_Icons = {};
			var reIcons = /googleMaps_Icons\["(.*?)"\] = ({.*?});/g;
			while( (result = reIcons.exec(script)) )
			{
				// Simulate that it was included in the config:
				this.editor.config.googleMaps_Icons[ result[1] ] = JSON.parse( result[2] );
			}
		}
	}

	return true;
};

// fixme: allow %
CKEGoogleMap.prototype.cssWidth = function()
{
	if (/\d+\D+/.test(this.width))
		return this.width;
	else
		return this.width + 'px';
};

CKEGoogleMap.prototype.cssHeight = function()
{
	if (/\d+\D+/.test(this.height))
		return this.height;
	else
		return this.height + 'px';
};

CKEGoogleMap.prototype.BuildScript = function()
{
	var handler = this.editor.plugins.googleMapsHandler,
		aScript = [], i,
		aPoints = [], point,
		aTexts = [],
		aLines = [], line,
		aAreas = [], area, areaLine;

	aScript.push('\r\n<script type="text/javascript">');
	aScript.push('/*<![CDATA[*/');
	aScript.push( '/* CK googlemaps v' + handler.majorVersion + '.' + handler.minorVersion +' */' );

	aScript.push('var imgMap = document.getElementById("' + this.number + '"),');
	aScript.push('	dMap = document.createElement("div");');
	aScript.push('imgMap.parentNode.insertBefore( dMap, imgMap);');
	aScript.push('dMap.appendChild(imgMap);');
	aScript.push('dMap.style.width = "' + this.cssWidth() + '";');
	aScript.push('dMap.style.height = "' + this.cssHeight() + '";');
	aScript.push('/*generatedType = ' + this.generatedType + ';*/');
	if (this.generatedType==2)
	{
		aScript.push('dMap.style.position = "relative";');
		aScript.push('dMap.style.cursor = "pointer";');
		aScript.push('dMap.onclick = function(e) {initLoader(e||event)};');
		aScript.push('var t = document.createTextNode("' + this.editor.lang.googlemaps.clickToLoad + '");');
		aScript.push('var d = document.createElement("div");');
		aScript.push('d.appendChild(t);');
		aScript.push('d.style.cssText="background-color:#e5e5e5; filter:alpha(opacity=80); opacity:0.8; padding:1em; font-weight:bold; text-align:center; position:absolute; left:0; width:' + this.width + 'px; top:0";');
		aScript.push('dMap.appendChild(d);');
	}

	aScript.push('function CreateGMap' + this.number + '() {');
	aScript.push('	var dMap = document.getElementById("' + this.number + '").parentNode;');
	aScript.push('	dMap.onclick = null;');

	aScript.push('	var mapOptions = {');
	aScript.push('		zoom: ' + this.zoom + ',');
	aScript.push('		center: [' + this.centerLat + ',' + this.centerLon + '],');
	aScript.push('		mapType: ' + this.mapType + ',' );
	aScript.push('		navigationControl: "' + this.zoomControl + '",' );
	aScript.push('		mapsControl: "' + this.mapTypeControl + '",' );
	aScript.push('		overviewMapControl: ' + this.overviewMapControl + ',' );
	aScript.push('		overviewMapControlOptions: {opened:' + this.overviewMapControlOpened + '},' );
	aScript.push('		scaleControl: ' + this.scaleControl + ',' );
	aScript.push('		weather: ' + this.weather + ',' );
	aScript.push('		traffic: ' + this.traffic + ',' );
	aScript.push('		transit: ' + this.transit + ',' );
	aScript.push('		googleBar: ' + this.googleBar );
	aScript.push('	};');

	aScript.push('	var myMap = new CKEMap(dMap, mapOptions);');

	var googleMaps_Icons = this.editor.config.googleMaps_Icons,
		usedIcons = {};

	if (this.markerPoints.length)
	{
		for (i=0; i<this.markerPoints.length; i++)
		{
			point = this.markerPoints[i];
			aPoints.push('{lat:' + point.lat + ', lon:' + point.lon + ', text:"' + this.encodeText(point.text) + '",color:"' + point.color +
				'", title:"' + this.encodeText(point.title) + '", maxWidth:' + point.maxWidth + ', open:' + (point.open ? 1 : 0) + '}');

			if ( googleMaps_Icons && googleMaps_Icons[ point.color ] )
				usedIcons[ point.color ] = googleMaps_Icons[ point.color ];
		}
		aScript.push('	myMap.AddMarkers( [' + aPoints.join(',\r\n') + '] );');
	}

	if (this.textsData.length)
	{
		for (i=0; i<this.textsData.length; i++)
		{
			point = this.textsData[i];
			aTexts.push('{lat:' + point.lat + ', lon:' + point.lon + ', title:"' + this.encodeText(point.title) + '", className:"' + this.encodeText(point.className)+ '"}');
		}
		aScript.push('	myMap.AddTexts( [' + aTexts.join(',\r\n') + '] );');
	}

	if (this.linesData.length)
	{
		for (i=0; i<this.linesData.length; i++)
		{
			line = this.linesData[i];
			aLines.push('{color:"' + line.color + '", weight:' + line.weight + ', opacity:' + line.opacity + ', points:"' + this.encodeText(line.points) + '", text:"' + this.encodeText(line.text) + '", maxWidth:' + line.maxWidth + '}');
		}
		aScript.push('	myMap.AddLines( [' + aLines.join(',\r\n') + '] );');
	}

	if (this.areasData.length)
	{
		for (i=0; i<this.areasData.length; i++)
		{
			area = this.areasData[i];
			areaLine = area.polylines[0];
			aAreas.push('{polylines: [{color:"' + areaLine.color + '", opacity:' + areaLine.opacity + ', weight:' + areaLine.weight + ', points:"' + this.encodeText(areaLine.points) + '"}], fill:true, color:"' + area.color + '", opacity:' + area.opacity + ', text:"' + this.encodeText(area.text) + '", maxWidth:' + area.maxWidth + '}');
		}
		aScript.push('	myMap.AddAreas( [' + aAreas.join(',\r\n') + '] );');
	}

	if (this.kmlOverlay)
		aScript.push('	myMap.AddKml("' + this.kmlOverlay + '");');

	aScript.push('}');

	aScript.push('');

	if ( !CKEDITOR.tools.isEmpty( usedIcons ) )
	{
		aScript.push('if (!window.googleMaps_Icons) window.googleMaps_Icons = {};');
		for ( var iconName in usedIcons )
		{
			if ( usedIcons.hasOwnProperty( iconName ) )
			{
				aScript.push('window.googleMaps_Icons["' + iconName + '"] = ' + JSON.stringify( usedIcons[ iconName ] ) + ';');
			}
		}
	}


	aScript.push('if (!window.gmapsLoaders) window.gmapsLoaders = [];');
	aScript.push('window.gmapsLoaders.push(CreateGMap' + this.number  + ');');

	if (this.generatedType==3)
		aScript.push('window.gmapsAutoload=true;');
	if (this.linesData.length || this.areasData.length)
		aScript.push('window.gmapsGeometry=true;');
	if (this.weather)
		aScript.push('window.gmapsWeather=true;');

	aScript.push('/*]]>*/');
	aScript.push('</script>');

	return aScript.join('\r\n');
};





CKEDITOR.plugins.add( 'googlemaps',
{
	requires : [ 'dialog' ],
	// translations
	lang : ['en', 'ar', 'cs', 'de', 'el', 'es', 'fi', 'fr', 'it', 'nl', 'pl', 'sk', 'tr'],
	// customize the context menu
	beforeInit : function( editor )
	{
	},
	init : function( editor )
	{
		editor.plugins.googleMapsHandler = new GoogleMapsHandler( editor );
		// Add buttons.
		editor.addCommand( 'googlemaps', new CKEDITOR.dialogCommand( 'googlemaps' ) );
		editor.ui.addButton( 'GoogleMaps',
			{
				label : editor.lang.googlemaps.toolbar,
				command : 'googlemaps',
				icon : this.path + 'icon.gif'
			} );

		CKEDITOR.dialog.add( 'googlemaps', this.path + 'dialogs/googlemaps.js' );
		CKEDITOR.dialog.add( 'googlemapsMarker', this.path + 'dialogs/marker.js' );
		CKEDITOR.dialog.add( 'googlemapsIcons', this.path + 'dialogs/icons.js' );
		CKEDITOR.dialog.add( 'googlemapsText', this.path + 'dialogs/text.js' );
		CKEDITOR.dialog.add( 'googlemapsLine', this.path + 'dialogs/line.js' );
		CKEDITOR.dialog.add( 'googlemapsArea', this.path + 'dialogs/area.js' );

		// If the "menu" plugin is loaded, register the menu items.
		if ( editor.addMenuItems )
		{
			editor.addMenuItems(
				{
					googlemaps :
					{
						label : editor.lang.googlemaps.menu,
						command : 'googlemaps',
						group : 'image',
						order : 1,
						icon : this.path + 'icon.gif'
					}
				});
		}

		// If the "contextmenu" plugin is loaded, register the listeners.
		if ( editor.contextMenu )
		{
			// check the image

			// We put our listener as the first item
			// The aim of this listener is to mark the images as "fake", but only while the context menu is displayed
			editor.contextMenu._.listeners.unshift( function( element, selection )
				{
					if ( !element || !element.is( 'img' ) || !editor.plugins.googleMapsHandler.isStaticImage( element.$ ) )
						return null;

					// Set here the trick
					element.data( 'cke-realelement', "1" );
					return null;
				});

			editor.contextMenu.addListener( function( element, selection )
				{
					if ( !element || !element.is( 'img' ) || !editor.plugins.googleMapsHandler.isStaticImage( element.$ ) )
						return null;

					// Now we clear it
					element.data( 'cke-realelement', false );

					// And say that this context menu item must be shown
					return { googlemaps : CKEDITOR.TRISTATE_ON };
				});
		}

		// Open our dialog on double click
		editor.on( 'doubleclick', function( evt )
			{
				var element = evt.data.element;

				if ( element.is( 'img' ) && editor.plugins.googleMapsHandler.isStaticImage( element.$ ) )
				{
					evt.data.dialog = 'googlemaps';
					evt.stop();
				}
			});

	},
	afterInit : function(editor)
	{
		// We need to control if the scripts must be added or removed
		processScriptOutput(editor);


		// Expand the editor to provide controlled dialogs.
		editor.openNestedDialog = function( dialogName, callbackOpen, callbackOK )
		{
			var onOk = function()
			{
				releaseHandlers( this );
				callbackOK && callbackOK( this );
			};
			var onCancel = function()
			{
				releaseHandlers( this );
			};
			var releaseHandlers = function( dialog )
			{
				dialog.removeListener( 'ok', onOk );
				dialog.removeListener( 'cancel', onCancel );
			};
			var bindToDialog = function( dialog )
			{
				dialog.on( 'ok', onOk );
				dialog.on( 'cancel', onCancel );
				callbackOpen && callbackOpen( dialog );
			};

			if ( editor._.storedDialogs[ dialogName ])
				bindToDialog( editor._.storedDialogs[ dialogName ] );
			else
			{
				CKEDITOR.on( 'dialogDefinition', function( e )
				{
					if ( e.data.name != dialogName )
						return;

					var definition = e.data.definition;

					e.removeListener();
					definition.onLoad = CKEDITOR.tools.override( definition.onLoad, function( original )
					{
						return function()
						{
							definition.onLoad = original;
							if ( typeof original == 'function' )
								original.call( this );
							bindToDialog( this );
						};
					} );
				});
			}

			editor.openDialog( dialogName );
		};


		// Workaround for #9060
		if (CKEDITOR.env.ie)
		{
			CKEDITOR.dialog.prototype.hide = CKEDITOR.tools.override( CKEDITOR.dialog.prototype.hide , function( originalFunction )
			{
				return function()
					{
						var parent = this._.parentDialog;

						originalFunction.call( this );

						if (parent && this._.editor != parent._.editor)
						{
							if ( this._.editor.mode == 'wysiwyg' )
							{
								var selection = this._.editor.getSelection();
								selection && selection.unlock( true );
							}
						}
					};
			});
		}

	}
});


// Adjust the output
function processScriptOutput( editor )
{
	var dataProcessor = editor.dataProcessor,
		htmlFilter = dataProcessor && dataProcessor.htmlFilter,
		dataFilter = dataProcessor && dataProcessor.dataFilter,
		handler = editor.plugins.googleMapsHandler;

	// dataFilter : conversion from html input to internal data
	dataFilter.addRules(
		{
			comment : function( contents )
			{
				// Check if they are part of our scripts. Parse them and remove from the content.
				var protectedSourceMarker = '{cke_protected}';

				if (contents.substr(0, protectedSourceMarker.length)== protectedSourceMarker)
				{
					var data = contents.substr( protectedSourceMarker.length );
					data = decodeURIComponent( data );

					if ( handler.detectMapScript( data ) )
					{
						var oMap = handler.createNew();
						if (oMap.parse( data ) )
						{
							if (oMap.requiresImage)
							{
								// Insert an image with the map data
								return new CKEDITOR.htmlParser.element( 'img',
									{
										id: oMap.number,
										mapnumber: oMap.number,
										src: oMap.generateStaticMap(),
										width: oMap.width,
										height: oMap.height
									} );
							}
							return null;
						}
					}
					else
					{
						if ( handler.detectGoogleScript( data ) )
							return null;
					}
				}

				return contents;
			},

			elements :
			{
				'img' : function(element)
				{
					var id = element.attributes['id'];
					if (id && (/^gmap\d+$/).test(id))
						element.attributes['mapnumber'] = id;
					else
					{
						var parent = element.parent;
						if (parent && parent.name=='div')
						{
							id = parent.attributes['id'];
							if (id && (/^dgmap\d+$/).test(id))
							{
								element.attributes['id'] = element.attributes['mapnumber'] = id.substr(1);
							}
						}
					}
				},

				'div' : function(element)
				{
					// Readjust previous ids.
					var id = element.attributes['id'];
					if (id && (/^gmap\d+$/).test(id))
					{
						element.attributes['id'] = 'd' + id;
					}
				}
			}

		}
	);

	// htmlFilter : conversion from internal data to html output.
	htmlFilter.addRules(
		{
			elements :
			{
				'img' : function( element )
				{
					var number = element.attributes.mapnumber;
					if (number)
					{
						var scriptNode,
							handler = editor.plugins.googleMapsHandler,
							oMap = handler.getMap( number );

						if (oMap && oMap.generatedType>1)
						{
							handler.CreatedMapsNames.push( oMap.number );
							// Inject the <script> for this map
							scriptNode = new CKEDITOR.htmlParser.cdata( oMap.BuildScript() );
							element.parent.children.push( scriptNode );
						}
						delete element.attributes.mapnumber;
					}

					return element;
				}
			}
		});

	dataProcessor.toDataFormat = CKEDITOR.tools.override( dataProcessor.toDataFormat , function( originalFunction )
	{
		return function( html, fixForBody )
			{
				// Check if we need to include or not the global maps building script
				var handler = editor.plugins.googleMapsHandler;
				handler.CreatedMapsNames = [];

				// Validate that everything is properly nested and there aren't left-over divs.
				if (!handler.validateDOM())
				{
					var config = editor.config,
						fullPage = config.fullPage,
						doc = editor.document;
					// Get the new HTML
					html = fullPage ? doc.getDocumentElement().getOuterHtml() : doc.getBody().getHtml();
				}

				var Result = originalFunction.call(this, html, fixForBody);

				if (handler.CreatedMapsNames.length > 0)
					Result += handler.BuildEndingScript();

				return Result;
			};
	});
}

})();